winbrew_app\operations\update/
patch.rs1use anyhow::{Context, Result, bail};
2use rusqlite::Connection;
3use std::fs;
4use std::io::{Cursor, Read};
5use std::path::Path;
6use zstd::stream::read::Decoder;
7
8use crate::core::network::Client;
9
10use super::metadata::{build_catalog_metadata_from_connection, verify_catalog_hash};
11
12pub(super) fn apply_catalog_patch_release(
41 client: &Client,
42 catalog_path: &Path,
43 catalog_temp_path: &Path,
44 metadata_temp_path: &Path,
45 patch_urls: &[String],
46 expected_hash: &str,
47 previous_hash: &str,
48) -> Result<()> {
49 if !catalog_path.exists() {
50 bail!("cannot apply catalog patch without an existing catalog database");
51 }
52
53 fs::copy(catalog_path, catalog_temp_path)
54 .context("failed to back up local catalog database for patch update")?;
55
56 let connection =
57 Connection::open(catalog_temp_path).context("failed to open catalog patch working copy")?;
58 connection
59 .pragma_update(None, "journal_mode", "DELETE")
60 .context("failed to set catalog patch journal mode")?;
61 connection
62 .execute_batch("PRAGMA foreign_keys = ON;")
63 .context("failed to enable foreign keys for catalog patch update")?;
64
65 for patch_url in patch_urls {
66 let patch_sql = download_catalog_patch_sql(client, patch_url)?;
67 connection
68 .execute_batch(&patch_sql)
69 .with_context(|| format!("failed to apply catalog patch from {patch_url}"))?;
70 }
71
72 let integrity_check: String = connection
73 .query_row("PRAGMA integrity_check", [], |row| row.get(0))
74 .context("failed to run catalog integrity check after patch application")?;
75
76 if integrity_check.trim() != "ok" {
77 bail!("catalog integrity check failed after patch application: {integrity_check}");
78 }
79
80 let metadata =
81 build_catalog_metadata_from_connection(&connection, expected_hash, previous_hash)?;
82
83 drop(connection);
84
85 verify_catalog_hash(catalog_temp_path, &metadata.current_hash)?;
86
87 fs::write(
88 metadata_temp_path,
89 serde_json::to_vec_pretty(&metadata)
90 .context("failed to serialize patched catalog metadata")?,
91 )
92 .context("failed to write patched catalog metadata")?;
93
94 Ok(())
95}
96
97fn download_catalog_patch_sql(client: &Client, patch_url: &str) -> Result<String> {
107 let response = client
108 .get(patch_url.to_string())
109 .send()
110 .with_context(|| format!("failed to send catalog patch request to {patch_url}"))?;
111 let response = response
112 .error_for_status()
113 .with_context(|| format!("catalog patch request failed for {patch_url}"))?;
114
115 let patch_bytes = response
116 .bytes()
117 .with_context(|| format!("failed to read catalog patch response from {patch_url}"))?;
118
119 let mut decoder = Decoder::new(Cursor::new(patch_bytes))
120 .context("failed to decompress catalog patch payload")?;
121 let mut patch_sql = String::new();
122 decoder
123 .read_to_string(&mut patch_sql)
124 .context("failed to decode catalog patch SQL")?;
125
126 Ok(patch_sql)
127}